上一篇講到:
[...items]
以及:
{ ...data }
它們常常被用來建立新的 Array 或 Object。
到了 React 裡,這種寫法更常出現:
setMemos(prev => [
...prev,
newMemo,
]);
第一次看到時,我其實很容易想:
為什麼要這麼麻煩?
因為 JavaScript 明明有:
push()
直接:
memos.push(newMemo);
不是更簡單嗎?
而且 push() 也真的有成功把資料加進去。
問題就在這裡:
「資料有被改到」和「React 正確知道資料更新了」是兩件事。
push()假設:
const memos = ["買牛奶"];
執行:
memos.push("寫文章");
現在:
memos
確實變成:
["買牛奶", "寫文章"]
所以:
push()本身完全沒有錯。
它的工作就是:
直接修改原本的 Array。
這種「直接修改原資料」的行為,通常稱為:
Mutation
例如:
const user = {
name: "小潔",
};
user.name = "狗勾";
原本的:
user
直接被修改了。
Array 也是:
const items = [1, 2];
items.push(3);
原本:
items
被改成:
[1, 2, 3]
所以:
push()
直接改 Object property
splice()
sort()
這類操作都有可能直接修改原資料。
假設:
const [memos, setMemos] =
useState(["買牛奶"]);
如果直接:
memos.push("寫文章");
JavaScript 裡的 memos 確實被改了。
但是我們沒有呼叫:
setMemos()
React 不會因為我們偷偷改了 Array 裡面的內容,就自動知道:
「欸,State 更新了,我現在要重新 Render。」
所以可能出現:
資料其實改了
↓
React 沒有正常收到 State 更新通知
↓
畫面沒有照預期更新
這就是第一個問題。
push() 完再 setMemos(memos) 不就好了?例如:
memos.push("寫文章");
setMemos(memos);
看起來合理很多。
但還有一個問題:
memos
前後其實還是同一個 Array。
可以把它想成:
修改前
memos ─────→ Array A
push()
memos ─────→ Array A
↑
還是同一份
內容變了,
但 Array 本身的 reference 沒有變。
例如:
const a = [1, 2];
const b = a;
這時:
a === b
結果是:
true
因為:
a
b
其實都指向同一個 Array。
圖像化:
a ───┐
↓
[1, 2]
↑
b ───┘
如果:
b.push(3);
那:
a
也會變成:
[1, 2, 3]
因為根本就是同一份資料。
如果改成:
const b = [...a];
現在:
a === b
結果是:
false
因為:
a ─────→ Array A
[1, 2]
b ─────→ Array B
[1, 2]
內容一樣,
但它們是兩個不同的 Array。
所以 React State 更新常看到:
setMemos(prev => [
...prev,
"寫文章",
]);
流程:
prev
↓
舊 Array
...prev
↓
把舊內容展開
加入新資料
↓
建立新的 Array
setMemos()
↓
React 收到新的 State
React 裡很常聽到:
Immutability
先不用把它想得很學術。
目前可以理解成:
不要直接修改原本的 State,而是根據舊資料建立一份新的資料。
例如:
user.name = "New Name";
setUser(user);
setUser(prev => ({
...prev,
name: "New Name",
}));
原本:
{
name: "小潔",
age: 31,
}
變成一個新的 Object:
{
name: "New Name",
age: 31,
}
React 很依賴資料前後的變化來判斷:
這份資料是不是更新了?
如果:
舊 State
↓
Array A
新 State
↓
還是 Array A
變化會比較難追蹤。
但:
舊 State
↓
Array A
新 State
↓
Array B
就很清楚:
這是一份新的資料。
所以:
[...prev]
或:
{ ...prev }
不只是「寫法比較潮」。
它其實是在幫我們建立新的 reference。
假設:
const items = [
{ id: 1, name: "A" },
{ id: 2, name: "B" },
];
不要直接:
items.push(newItem);
可以:
const next = [
...items,
newItem,
];
不要直接:
items.splice(index, 1);
可以用:
const next = items.filter(
item => item.id !== targetId
);
因為:
filter()
會回傳新的 Array。
可以:
const next = items.map(item =>
item.id === targetId
? {
...item,
name: "New Name",
}
: item
);
流程:
每一筆 item
↓
是我要改的嗎?
不是
→ 原樣回傳
是
→ 建立新的 Object
→ 修改 name
最後:
map()
↓
產生新的 Array
map() 為什麼在 React 很常見前幾篇講 map() 時,我們說:
把 Array 轉換成新的 Array。
到了 State 更新裡,它又剛好非常適合:
舊 Array
↓
map()
↓
新的 Array
例如:
setItems(prev =>
prev.map(item =>
item.id === targetId
? {
...item,
name: "Updated",
}
: item
)
);
可以拆成:
拿舊 items
↓
逐筆檢查
↓
找到 target
↓
建立新的 item
↓
其餘維持原資料
↓
組成新的 Array
↓
setItems()
這些前面學過的東西其實都開始接起來了:
map()
+
Spread
+
State
+
Immutability
例如:
const user = {
name: "小潔",
company: {
name: "ABC",
},
};
如果:
const next = {
...user,
};
我們昨天有提過:
這只是 Shallow Copy。
所以:
next.company.name = "XYZ";
仍然可能改到原本的:
user.company.name
因為:
user.company
跟:
next.company
還是同一份 Object。
例如:
setUser(prev => ({
...prev,
company: {
...prev.company,
name: "XYZ",
},
}));
流程:
舊 user
↓
建立新的 user
舊 company
↓
建立新的 company
name
↓
改成 XYZ
最後:
新的 user
└─ 新的 company
而不是偷偷修改原來的巢狀 Object。
如果資料可以到處直接被修改:
data.name = ...
data.company.name = ...
items.push(...)
items.splice(...)
當某個值突然變掉時,很難知道:
到底是哪一段程式改的?
但如果都透過:
建立新資料
↓
setState
資料變化的入口會比較清楚。
例如:
setItems(...)
就可以直接找到:
這裡發生了一次 State 更新。
所以 Immutability 不只是 React Render 的問題。
它也讓:
資料流
State 變更
Debug
比較容易追。
這裡也要分清楚。
不是說:
push()
splice()
sort()
都是壞東西。
例如:
const temp = [];
temp.push("A");
temp.push("B");
如果它只是一個區域變數,而且沒有 React State 的問題,完全可以使用。
重點不是:
push()永遠不能用。
而是:
不要直接拿 Mutation 的方式修改 React State。
例如:
setItems(...)
我會看:
新的值是不是新 Array / Object?
還是程式其實先修改了原本 State?
例如:
items.push(newItem);
setItems(items);
我會警覺:
原本 items 被 mutate 了
setItems(prev => [
...prev,
newItem,
]);
可以理解成:
根據舊資料
↓
建立新 Array
↓
交給 React
到這裡其實已經可以看到:
Day 15
map()
→ Array 轉成新的 Array
Day 16
Spread
→ 根據舊資料建立新 Array / Object
Day 17
Immutability
→ 不直接修改 State
→ 建立新資料再更新
所以這些語法不是三個獨立的知識點。
它們在 React 裡其實常常一起出現:
setItems(prev =>
prev.map(item =>
item.id === targetId
? {
...item,
name: "Updated",
}
: item
)
);
現在再看這段,可以拆成:
prev
→ 舊 State
map()
→ 建立新 Array
item.id === targetId
→ 找到要改的資料
...item
→ 保留原欄位
name: "Updated"
→ 覆蓋其中一個欄位
setItems()
→ 更新 State
原本像咒語的一整坨,其實每一塊都有自己的工作。
Mutation
→ 直接改原本資料
Immutability
→ 不直接改原本資料
→ 建立新的 Array / Object
在 React State 裡,通常會偏向後者。
所以:
push()
不是因為「不能新增資料」。
而是它會:
直接修改原本 Array。
React 更常希望我們:
舊 State
↓
建立新的資料
↓
setState()
↓
React Render
當我理解這件事後,再看到滿畫面的:
...
map()
filter()
也比較不會覺得工程師只是故意把程式寫複雜。
很多時候,它們其實都是在做同一件事:
讓資料的變化保持清楚、可追蹤。
下一篇可以接一個在 React List 裡同樣很常見的問題:
key 到底是幹嘛的?為什麼不能每次都直接用 index?